package playsManager.database;

import java.io.*;

import java.nio.file.*;
import java.util.*;

import playsManager.*;

public class Database {
	
	protected Path path;

	//class invariant: "path" is the path to a directory in which the user
	//has read/write access.
	
	//class invariant: GetAdminsTable().Size() > 0
	
	private static Database instance;
	public static Database GetInstance() throws Exception
	{
		//precondition: user has read/write access to current directory
		//postcondition: an instance of the Database class is returned
		
		if(instance == null)
			instance = new Database();
		return instance;
	}
	
	private Database() throws IOException
	{
		//precondition: user has read/write access to current directory
		//postcondition: an instance of the Database class is returned
		
		this(
				Paths.get(
							new File(".").getAbsolutePath()
						 )
					 .resolve("database")
			);
	}
	private Database(Path databasePath) throws IOException
	{
		//precondition: databasePath is a Path object that represents a valid path to use as a database.  the directory does not need to exist - it will be created.
		//				the user should have sufficient rights to create such a directory if it does not exist.
		//postcondition: database instance is returned.
		
		path = databasePath;
		File pathFile = new File(path.toString());
		if(!pathFile.exists())
			Files.createDirectory(path);
	}
	
	public void AddAdministrator(Administrator administrator) throws Exception
	{
		//to indicate success, don't throw exception.
		//precondition: administrator is an Administrator populated with appropriate values.
		//postcondition: administrator is added to the database.
		
		ArrayList<Administrator> admins = GetAdminsTable();
		administrator.id = GetNextAdminId();
		admins.add(administrator);
	}
	public Administrator GetAdministrator(int id) throws Exception
	{
		//precondition: id is the id of an existing administrator.
		//postcondition: return value is the administrator with the id specified.
		
		ArrayList<Administrator> admins = GetAdminsTable();
		return FindItem(admins, id);
	}
	public ArrayList<Administrator> GetAllAdministrators() throws Exception
	{
		//return value: an ArrayList filled with all Plays in the database.
		
		return GetAdminsTable();
	}
	public void DeleteAdministrator(int id) throws Exception
	{
		//to indicate success, don't throw exception
		//precondition: id is the id of an existing administrator
		//postcondition: admin deleted from database
		
		ArrayList<Administrator> admins = GetAdminsTable();
		admins.remove(FindItem(admins, id));
	}
	private int GetNextAdminId() throws Exception
	{
		//postcondition: an integer fit for use as an
		//identifier for a new theater, which isn't in
		//use by any other theater in the database.
		
		return GetNextId(GetAdminsTable());
	}
	
	public void AddTheater(Theater theater) throws Exception
	{
		//to indicate success, don't throw exception
		//precondition: theater is a Theater populated with appropriate values.
		//postcondition: theater added to database  
		
		ArrayList<Theater> items = GetTheatersTable();
		theater.id = GetNextTheaterId();
		items.add(theater);
	}
	public Theater GetTheater(int id) throws Exception
	{
		//to indicate success, don't throw an exception.
		//precondition: id is the id of an existing theater.
		//postcondition: theater is returned.
		
		ArrayList<Theater> theaters = GetTheatersTable();
		return FindItem(theaters, id);
	}
	public ArrayList<Theater> GetAllTheaters() throws Exception
	{
		//return value: an ArrayList filled with all Theaters in the database.
		
		return GetTheatersTable();
	}
	public void DeleteTheater(int id) throws Exception
	{
		//precondition: id is the id of an existing play
		//postcondition: the theater and all its plays are deleted
		
		//delete all plays belonging to the theater
		ArrayList<Play> plays = GetPlaysTable();
		for(Play p : plays)
			if(p.GetTheaterId() == id)
				plays.remove(FindItem(plays, id));

		//delete the theater
		ArrayList<Theater> theaters = GetTheatersTable();
		Theater theater = FindItem(theaters, id);
		theaters.remove(theater);
	}
	private int GetNextTheaterId() throws Exception
	{
		//postcondition: an integer fit for use as an
		//identifier for a new theater, which isn't in
		//use by any other theater in the database. 
		return GetNextId(GetTheatersTable());
	}
	
	public void AddPlay(Play play) throws Exception
	{
		//to indicate success, don't throw exception
		//precondition: play is a Play populated with appropriate values.
		
		ArrayList<Play> items = GetPlaysTable();
		play.SetId(GetNextPlayId());
		items.add(play);
	}
	public Play GetPlay(int id) throws Exception
	{
		//precondition: id is the id of an existing Play.
		//postcondition: the play with the id specified is returned.
		
		ArrayList<Play> plays = GetPlaysTable();
		return FindItem(plays, id);
	}
	public ArrayList<Play> GetAllPlays() throws Exception
	{
		//return value: an ArrayList filled with all Plays in the database.
		
		return GetPlaysTable();
	}
	public void DeletePlay(int id) throws Exception
	{
		//precondition: id is the id of an existing play.
		//postcondition: play is deleted from the database.
		ArrayList<Play> plays = GetPlaysTable();
		plays.remove(FindItem(plays, id));
	}
	private int GetNextPlayId() throws Exception
	{
		//postcondition: an integer fit for use as an
		//identifier for a new play, which isn't in
		//use by any other play in the database.
		
		return GetNextId(GetPlaysTable());
	}
	
	public AdminLogon LoginAsAdmin(LogonInfo info)
	{
		//precondition: info is a LogonInfo populated with logon information.
		//return value: a populated AdminLogon object if the logon was successful, otherwise null. 
		
		try
		{
			AdminLogon result = new AdminLogon();
			
			ArrayList<Administrator> list = GetAdminsTable();
			for(Administrator item : list)
			{
				if(item.username.equals(info.username) && item.password.equals(info.password))
				{
					result.id = item.id;
					result.username = item.username;
					return result;
				}
			}
		}
		catch(Exception ex)
		{
			
		}
		
		return null;
	}
	public TheaterLogon LoginAsTheater(LogonInfo info)
	{
		//precondition: info is a LogonInfo populated with logon information.
		//return value: a populated TheaterLogon object if the logon was successful, otherwise null.
		
		try
		{
			TheaterLogon result = new TheaterLogon();
			
			ArrayList<Theater> list = GetTheatersTable();
			for(Theater item : list)
			{
				if(item.username.equals(info.username) && item.password.equals(info.password))
				{
					result.id = item.id;
					result.username = item.username;
					return result;
				}
			}
		}
		catch(Exception ex)
		{
			
		}
		
		return null;
	}
	
	ArrayList<Administrator> tempAdministrators;
	ArrayList<Theater> tempTheaters;
	ArrayList<Play> tempPlays;
	
	protected ArrayList<Play> GetPlaysTable() throws ClassNotFoundException, Exception
	{
		//postcondition: an arraylist of plays in the database.
		
		if(tempPlays == null)
			return tempPlays = OpenTable("plays");
		else
			return tempPlays;
	}
	
	protected ArrayList<Theater> GetTheatersTable() throws Exception
	{
		//postcondition: an arraylist of theaters in the database.
		
		if(tempTheaters == null)
			return tempTheaters = OpenTable("theaters");
		else
			return tempTheaters;
	}
	
	protected ArrayList<Administrator> GetAdminsTable() throws Exception
	{
		//precondition: the admins file does not exist, or the admins file
		//				does exist *and* there is at least one admin in the file 
		//postcondition: an arraylist of admins in the database.  there is at
		//				least one item in the array.
		
		if(tempAdministrators == null)
		{
			tempAdministrators = OpenTable("admins");
			if(tempAdministrators.size() == 0)
			{
				Administrator a = new Administrator();
				a.id = 1;
				a.username = "admin";
				a.password = "admin";
				tempAdministrators.add(a);
			}
			return tempAdministrators;
		}
		else
			return tempAdministrators;
	}
	
	protected <T> void SaveTable(String tableName, ArrayList<T> data) throws Exception
	{
		//precondition: tableName is the valid name a file (no characters not allowed in a filename, e.g.)
		// 				(the file may or may not exist)
		
		FileOutputStream fos = new FileOutputStream(path.resolve(tableName).toString()); 
		ObjectOutputStream out = new ObjectOutputStream(fos);
		
		out.writeObject(data);
		out.close();
	}
	
	protected <T> ArrayList<T> OpenTable(String tableName) throws Exception
	{
		//precondition: tableName is the valid name a file (no characters not allowed in a filename, e.g.)
		// 				(the file may or may not exist)
		//				tableName.equals("admins") || tableName.equals("theaters") || tableName.equals("plays")
		
		Path p = path.resolve(tableName);
		if(Files.exists(p))
		{
			FileInputStream fis = new FileInputStream(p.toString()); 
			ObjectInputStream in = new ObjectInputStream(fis);
			
			@SuppressWarnings("unchecked")
			ArrayList<T> result = (ArrayList<T>)in.readObject();
			
			in.close();
			return result;
		}
		else
			return new ArrayList<T>();
	}
	
	
	public <E extends DatabaseItem> E FindItem(ArrayList<E> list, int id) throws Exception
	{
		//precondition: "list" is an ArrayList with exactly one DatabaseItem that has
		//				an id equal to "id".  list != null.
		//postcondition: the DatabaseItem which has the specified id.
		//				 <result>.id.equals(id)
		
		for(E i : list)
		{
			if(i.GetId() == id)
			{
				return i;
			}
		}
		
		throw new Exception("no item exists with this id.");
	}
	
	private <T extends DatabaseItem> int GetNextId(ArrayList<T> table)
	{
		//precondition: table != null, and the elements of the table are
		//				sorted by ascending id.
		//postcondition: one + the highest occurring id in the list, or
		//				if there are no elements in the list, 1.
		
		if(table.size() > 0)
			return table.get(table.size() - 1).GetId() + 1;
		else
			return 1;
	}

	public void SaveChanges() throws Exception
	{
		//precondition: see class invariants.
		//postcondition: database is saved to disk.
		
		SaveTable("admins", GetAdminsTable());
		SaveTable("theaters", GetTheatersTable());
		SaveTable("plays", GetPlaysTable());
	}
}
